﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo ("MyTestProject")]

/**
 * パックされた形式(RGB565,RGB551,RGBA4444)について。
 * 現在の実装だと全てRGBまたはRGBAにアンパックされて保存される。
 * 従って現状これらの形式を選択するメリットは何もない。
 * 将来的には変わるかもしれない。
 * */

namespace M3G {
    public abstract class ImageBase : Object3D {

        #region Constant
        // Core format
        public const int Alpha          = 96;
        public const int Luminance      = 97;
        public const int LuminanceAlpha = 98;
        public const int RGB            = 99;
        public const int RGBA           = 100;
        public const int RGB565         = 101;
        public const int RGBA5551       = 102;
        public const int RGBA4444       = 103;

        // Advanced format
        public const int RGBETC         = 104;  // 未対応
        public const int RCompressed    = 105;  // 未対応
        public const int RGCompressed   = 106;  // 未対応
        public const int RGBCompressed  = 107;  // 未対応
        public const int RGBACompressed = 108;  // 未対応

        // option
        public const int LossLess       = 16384;   // 未対応
        public const int NoMipmap       = 32768;
        public const int YUp            = 65536;
        #endregion

        #region Field
        int    format;
        int    option;
        int    width;
        int    height;
        int    bpp;
        bool   mutable;
        bool   autoMipmapGeneration;
        Dictionary<int, byte[][]> pixels;   // = [name][miplevel][width*height*bpp]
        #endregion

        #region Method

        /**
         * コンストラクタ
         * 画像を保管するメモリは確保しない。
         * */
        public ImageBase (int format, int width, int height) {
            if ((format & 0x7f) < ImageBase.Alpha || (format & 0x7f) > ImageBase.RGBA4444) {
                throw new ArgumentException ("Image format must be Alpha,Luminance, LuminanceAlpha,RGB,RGBA,RGB565,RGBA5551,RGBA4444, format=" + (format & 0x7f));
            }
            if (width <= 0 || height <= 0) {
                throw new ArgumentException ("Image size is invalid, w=" + width + ",h=" + height);
            }
            if ((width & (width - 1)) != 0 || (height & (height - 1)) != 0) {
                throw new ArgumentException ("Image size must be power of 2, width=" + width + ",height=" + height);
            }

            this.format = format & 0x7f;
            this.option = format & ~0x7f;
            this.width = width;
            this.height = height;
            this.bpp = FormatToBpp (this.format);
            this.mutable = true;
            this.autoMipmapGeneration = true;
            this.pixels = new Dictionary<int, byte[][]> ();
        }

        /**
         * 指定の名前の画像データの保存領域を作成する
         * */
        internal void AllocateImage (int name) {
            var pixels = new byte[MaxMipLevel + 1][];
            for (var m=0; m <= MaxMipLevel; m++) {
                var w = (int)(width / Math.Pow (2, m));
                var h = (int)(height / Math.Pow (2, m));
                w = (w == 0) ? 1 : w;
                h = (h == 0) ? 1 : h;
                pixels[m] = new byte[w * h * bpp];
                for (var i=0; i < pixels[m].Length; i++) {
                    pixels[m][i] = 255;
                }
            }

            this.pixels.Add (name, pixels);
        }

        internal int ImageCount {
            get { return pixels.Count (); }
        }

        /**
         * ベースレベルから全てのミップマップ階層の画像を作成する
         * */
        internal void GenerateMipmap (int name) {
            for (var m=1; m <= MaxMipLevel; m++) {
                var width  = MiplevelWidth (m);
                var height = MiplevelHeight (m);
                for (var y=0; y < height; y++) {
                    for (var x=0; x < width; x++) {
                        for (var i=0; i < bpp; i++) {
                            var x2 = x * 2;
                            var y2 = y * 2;
                            var w2 = width * 2;
                            var c1 = pixels[name][m - 1][((y2 + 0) * w2 + (x2 + 0)) * bpp + i];
                            var c2 = pixels[name][m - 1][((y2 + 0) * w2 + (x2 + 1)) * bpp + i];
                            var c3 = pixels[name][m - 1][((y2 + 1) * w2 + (x2 + 0)) * bpp + i];
                            var c4 = pixels[name][m - 1][((y2 + 1) * w2 + (x2 + 1)) * bpp + i];
                            this.pixels[name][m][(y * width + x) * bpp + i] = (byte)((c1 + c2 + c3 + c4) / 4);
                        }

                    }
                }
            }
        }


        /**
         * コピーコンストラクタ
         * */
        protected ImageBase (ImageBase image)
            : base (image) {
            this.format = image.format;
            this.option = image.option;
            this.width = image.width;
            this.height = image.height;
            this.bpp = image.bpp;
            this.mutable = image.mutable;
            this.autoMipmapGeneration = image.autoMipmapGeneration;
            this.pixels = new Dictionary<int, byte[][]> (image.pixels);
            for (var f=0; f < pixels.Count; f++) {
                this.pixels[f] = image.pixels[f].Select (row => row.ToArray ()).ToArray ();
            }
        }

        public void Commit () {
            this.mutable = false;
        }

        public bool Mutable {
            get { return mutable; }
        }

        public int Format {
            get { return format; }
        }

        internal int Option {
            get { return option; }
        }

        public int Height {
            get { return height; }
        }

        public int Width {
            get { return width; }
        }

        internal int Bpp {
            get { return bpp; }
        }

        public bool MipmapGenerateEnabled {
            get { return autoMipmapGeneration; }
            set { autoMipmapGeneration = value; }
        }

        /**
         * 現状NoMipmapを指定されなければ常にミップマップは構築されている。
         * 将来的には異なる実装になるかも
         * */
        public bool Mipmapped {
            get { return ((option & NoMipmap) != 0) ? false : true; }
        }


        /**
         * 指定された名前、ミップマップレベルの全ての画像ピクセルを更新する
         * また自動ミップマップ作成が有効になっているときはミップマップ階層が更新される
         * */
        internal void SetPixels (int name, int mipmapLevel, byte[] pixels) {
            var width  = MiplevelWidth (mipmapLevel);
            var height = MiplevelHeight (mipmapLevel);
            for (var y=0; y < height; y++) {
                for (var x=0; x < width; x++) {
                    for (var i=0; i < bpp; i++) {
                        this.pixels[name][mipmapLevel][(y * width + x) * bpp + i] = pixels[(y * width + x) * bpp + i];
                    }
                }
            }
            if (mipmapLevel == 0 && Mipmapped & MipmapGenerateEnabled) {
                GenerateMipmap (name);
            }
        }

        /**
         * 指定された名前、ミップマップレベルの一部の画像ピクセルを更新する
         * また自動ミップマップ作成が有効になっているときはミップマップ階層が更新される
         * */
        internal void SetPixels (int name, int mipmapLevel, int x, int y, int width, int height, byte[] pixels) {
            var miplevelWidth = MiplevelWidth (mipmapLevel);

            for (var yy=0; yy < height; yy++) {
                for (var xx=0; xx < width; xx++) {
                    for (var i=0; i < bpp; i++) {
                        this.pixels[name][mipmapLevel][((y + yy) * miplevelWidth + (x + xx)) * bpp + i] = pixels[(yy * width + xx) * bpp + i];
                    }
                }
            }
            if (mipmapLevel == 0 && Mipmapped & MipmapGenerateEnabled) {
                GenerateMipmap (name);
            }
        }

        /**
         * 最大ミップレベルnの計算(nも有効)
         * */
        internal int MaxMipLevel {
            get {
                return (Option & NoMipmap) == 0 ? (int)Math.Log (Math.Max (width, height), 2.0) : 0;
            }
        }

        /**
         * ミップマップ階層(miplevel)での画像の幅を取得する
         * */
        internal int MiplevelWidth (int miplevel) {
            return (int)Math.Ceiling (Width / Math.Pow (2, miplevel));
        }

        /**
         * ミップマップ階層(miplevel)での画像の高さを取得する
         * */
        internal int MiplevelHeight (int miplevel) {
            return (int)Math.Ceiling (Height / Math.Pow (2, miplevel));
        }

        /*
         * 圧縮されたフォーマットも含めて必ず1バイト1コンポーネントで格納する
         * つまりbppはコンポーネント数と同じ
         * */
        internal static int FormatToBpp (int format) {
            switch (format) {
                case Alpha: return 1;
                case Luminance: return 1;
                case LuminanceAlpha: return 2;
                case RGB: return 3;
                case RGBA: return 4;
                case RGB565: return 3;     // (*1)
                case RGBA5551: return 4;   // (*1)
                case RGBA4444: return 4;   // (*1)
                default: return 0;
            }
        }

        /**
         * Bitmapオブジェクトをbyte[]に変換する
         * */
        internal static byte[] ConvertToPixels (int format, Bitmap bitmap) {
            return ConvertToPixels (format, 0, 0, bitmap.Width, bitmap.Height, bitmap);
        }

        /**
         * Bitmapオブジェクトの一部だけを取り出してbyte[]に変換する
         * */
        internal static byte[] ConvertToPixels (int format, int srcX, int srcY, int width, int height, Bitmap bitmap) {
            var bpp    = FormatToBpp (format);
            var pixels = new byte[width * height * bpp];
            for (var y=0; y < height; y++) {
                for (var x=0; x < width; x++) {
                    var color = bitmap.GetPixel (srcX + x, srcY + y);
                    switch (format) {
                        case Image2D.Alpha: {
                            pixels[(y * width + x) * bpp] = color.A;
                            break;
                        }
                        case Image2D.Luminance: {
                            pixels[(y * width + x) * bpp] = (byte)(((int)color.R + color.G + color.B) / 3);
                            break;
                        }
                        case Image2D.LuminanceAlpha: {
                            pixels[(y * width + x) * bpp + 0] = (byte)(((int)color.R + color.G + color.B) / 3);
                            pixels[(y * width + x) * bpp + 1] = color.A;
                            break;
                        }
                        case Image2D.RGB:
                        case Image2D.RGB565: {
                            pixels[(y * width + x) * bpp + 0] = color.R;
                            pixels[(y * width + x) * bpp + 1] = color.G;
                            pixels[(y * width + x) * bpp + 2] = color.B;
                            break;
                        }
                        case Image2D.RGBA:
                        case Image2D.RGBA5551:
                        case Image2D.RGBA4444: {
                            pixels[(y * width + x) * bpp + 0] = color.R;
                            pixels[(y * width + x) * bpp + 1] = color.G;
                            pixels[(y * width + x) * bpp + 2] = color.B;
                            pixels[(y * width + x) * bpp + 3] = color.A;
                            break;
                        }
                    }
                }
            }

            return pixels;
        }

        /**
         * ushort[]の画像データをbyte[]に変換する
         * */
        internal static byte[] ConvertToPixels (int format, ushort[] image) {
            var bpp    = FormatToBpp (format);
            var pixels = new byte[image.Length * bpp];

            for (var i=0; i < image.Length; i++) {
                switch (format) {
                    case Image2D.RGB565: {
                        pixels[i * bpp + 0] = (byte)(((image[i] & 0xf800) >> 11) / (31.0f) * 255.0f);
                        pixels[i * bpp + 1] = (byte)(((image[i] & 0x07e0) >> 5) / (63.0f) * 255.0f);
                        pixels[i * bpp + 2] = (byte)(((image[i] & 0x001f) >> 0) / (31.0f) * 255.0f);
                        break;
                    }
                    case Image2D.RGBA5551: {
                        pixels[i * bpp + 0] = (byte)(((image[i] & 0xf800) >> 11) / (31.0f) * 255.0f);
                        pixels[i * bpp + 1] = (byte)(((image[i] & 0x07c0) >> 6) / (31.0f) * 255.0f);
                        pixels[i * bpp + 2] = (byte)(((image[i] & 0x003e) >> 1) / (31.0f) * 255.0f);
                        pixels[i * bpp + 3] = (byte)(((image[i] & 0x0001) >> 0) / 255.0f);
                        break;
                    }
                    case Image2D.RGBA4444: {
                        pixels[i * bpp + 0] = (byte)(((image[i] & 0xf000) >> 12) / (15.0f) * 255.0f);
                        pixels[i * bpp + 1] = (byte)(((image[i] & 0x0f00) >> 8) / (15.0f) * 255.0f);
                        pixels[i * bpp + 2] = (byte)(((image[i] & 0x00f0) >> 4) / (15.0f) * 255.0f);
                        pixels[i * bpp + 3] = (byte)(((image[i] & 0x000f) >> 0) / (15.0f) * 255.0f);
                        break;
                    }
                }
            }

            return pixels;
        }

        /**
         * uint[]の画像データをbyte[]に変換する
         * */
        internal static byte[] ConvertToPixels (int format, uint[] image) {
            var bpp    = FormatToBpp (format);
            var pixels = new byte[image.Length * bpp];

            for (var i=0; i < image.Length; i++) {
                switch (format) {
                    case Image2D.RGB: {
                        pixels[i * bpp + 0] = (byte)((image[i] & 0x00ff0000) >> 16);
                        pixels[i * bpp + 1] = (byte)((image[i] & 0x0000ff00) >> 8);
                        pixels[i * bpp + 2] = (byte)((image[i] & 0x000000ff) >> 0);
                        break;
                    }
                    case Image2D.RGBA: {
                        pixels[i * bpp + 0] = (byte)((image[i] & 0x00ff0000) >> 16);
                        pixels[i * bpp + 1] = (byte)((image[i] & 0x0000ff00) >> 8);
                        pixels[i * bpp + 2] = (byte)((image[i] & 0x000000ff) >> 0);
                        pixels[i * bpp + 3] = (byte)((image[i] & 0xff000000) >> 24);
                        break;
                    }
                }
            }

            return pixels;
        }

        /**
         * PNG画像として保存する。
         * M3G非標準(デバッグ用)
         * */
        internal void Save (int name, int miplevel, String fileName) {
            var width = MiplevelWidth (miplevel);
            var height = MiplevelHeight (miplevel);
            var bitmap = new Bitmap (width, height, PixelFormat.Format32bppArgb);
            var pixels = this.pixels[name][miplevel];

            for (var y = 0; y < height; y++) {
                for (var x = 0; x < width; x++) {
                    switch (format) {
                        case Alpha: {
                            bitmap.SetPixel (x, y, System.Drawing.Color.FromArgb (pixels[(y * width + x) * bpp],
                                                                                          0,
                                                                                          0,
                                                                                          0));
                            break;
                        }
                        case Luminance: {
                            bitmap.SetPixel (x, y, System.Drawing.Color.FromArgb (255,
                                                                                              pixels[(y * width + x) * bpp],
                                                                                              pixels[(y * width + x) * bpp],
                                                                                              pixels[(y * width + x) * bpp]));
                            break;
                        }
                        case LuminanceAlpha: {
                            bitmap.SetPixel (x, y, System.Drawing.Color.FromArgb (pixels[(y * width + x) * bpp + 1],
                                                                                                   pixels[(y * width + x) * bpp + 0],
                                                                                                   pixels[(y * width + x) * bpp + 0],
                                                                                                   pixels[(y * width + x) * bpp + 0]));
                            break;
                        }
                        case RGB:
                        case RGB565: {
                            bitmap.SetPixel (x, y, System.Drawing.Color.FromArgb (255,
                                                                                           pixels[(y * width + x) * bpp + 0],
                                                                                           pixels[(y * width + x) * bpp + 1],
                                                                                           pixels[(y * width + x) * bpp + 2]));
                            break;
                        }
                        case RGBA:
                        case RGBA4444:
                        case RGBA5551: {
                            bitmap.SetPixel (x, y, System.Drawing.Color.FromArgb (pixels[(y * width + x) * bpp + 3],
                                                                                             pixels[(y * width + x) * bpp + 0],
                                                                                             pixels[(y * width + x) * bpp + 1],
                                                                                             pixels[(y * width + x) * bpp + 2]));
                            break;
                        }
                    }
                }
            }

            bitmap.Save (fileName);
        }

        /**
         * ミップマップ階層全体をPNG画像として保存する。
         * M3G非標準(デバッグ用)
         * */
        internal void SaveMipmapPyramid (int name, String fileName) {
            var bitmap = new Bitmap (width * 2, height * 2, PixelFormat.Format32bppArgb);
            var xoffset = 0;
            var yoffset = 0;

            for (var m=0; m <= MaxMipLevel; m++) {
                var pixels         = this.pixels[name][m];
                var miplevelWidth  = MiplevelWidth (m);
                var miplevelHeight = MiplevelHeight (m);
                for (var y = 0; y < miplevelHeight; y++) {
                    for (var x = 0; x < miplevelWidth; x++) {
                        var yy = y + yoffset;
                        var xx = x + xoffset;
                        switch (format) {
                            case Alpha: {
                                bitmap.SetPixel (xx, yy, System.Drawing.Color.FromArgb (pixels[(y * miplevelWidth + x) * bpp],
                                                                                        0,
                                                                                        0,
                                                                                        0));
                                break;
                            }
                            case Luminance: {
                                bitmap.SetPixel (xx, yy, System.Drawing.Color.FromArgb (255,
                                                                                        pixels[(y * miplevelWidth + x) * bpp],
                                                                                        pixels[(y * miplevelWidth + x) * bpp],
                                                                                        pixels[(y * miplevelWidth + x) * bpp]));
                                break;
                            }
                            case LuminanceAlpha: {
                                bitmap.SetPixel (xx, yy, System.Drawing.Color.FromArgb (pixels[(y * miplevelWidth + x) * bpp + 1],
                                                                                        pixels[(y * miplevelWidth + x) * bpp + 0],
                                                                                        pixels[(y * miplevelWidth + x) * bpp + 0],
                                                                                        pixels[(y * miplevelWidth + x) * bpp + 0]));
                                break;
                            }
                            case RGB:
                            case RGB565: {
                                bitmap.SetPixel (xx, yy, System.Drawing.Color.FromArgb (255,
                                                                                        pixels[(y * miplevelWidth + x) * bpp + 0],
                                                                                        pixels[(y * miplevelWidth + x) * bpp + 1],
                                                                                        pixels[(y * miplevelWidth + x) * bpp + 2]));
                                break;
                            }
                            case RGBA:
                            case RGBA4444:
                            case RGBA5551: {
                                bitmap.SetPixel (xx, yy, System.Drawing.Color.FromArgb (pixels[(y * miplevelWidth + x) * bpp + 3],
                                                                                        pixels[(y * miplevelWidth + x) * bpp + 0],
                                                                                        pixels[(y * miplevelWidth + x) * bpp + 1],
                                                                                        pixels[(y * miplevelWidth + x) * bpp + 2]));
                                break;
                            }
                        } // switch(format)
                    } // x
                } // y

                xoffset += miplevelWidth;
                yoffset += miplevelHeight;
            } // m

            bitmap.Save (fileName);
        }

        #endregion



    }
}
